import os
import re
import queue
import time
import threading
import webbrowser
import random
import requests
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime

from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
    QLabel, QTextEdit, QLineEdit, QCheckBox, QProgressBar, QTableWidget,
    QTableWidgetItem, QHeaderView, QFileDialog, QMessageBox, QDialog,
    QListWidget, QListWidgetItem, QSplitter, QMenu, QAction, QStyleFactory,
    QDesktopWidget, QFrame
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QUrl
from PyQt5.QtGui import QDesktopServices, QFont, QCursor

# --- Constants for File Paths (adjust if necessary) ---
TOKENS_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\токены для автопостинга.txt"
FAILED_TOKENS_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\Токены неуспешных публикаций.txt"
POSTS_DIR = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\посты"
UPLOADED_POSTS_DIR = os.path.join(POSTS_DIR, "загруженные")
AUTOPOST_STATE_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\autopost_state.txt"
POST_LIMIT_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\post_limit_state.txt"
PHOTO_COUNT_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\photo_count_state.txt"
LAST_POST_DATE_FILE_PATH = r"C:\Users\1\Desktop\VS 2022\Автопостинг ВК\last_post_date.txt"

# --- Dark Theme Stylesheet ---
DARK_STYLESHEET = """
    QMainWindow, QDialog {
        background-color: #2b2b2b;
    }
    QWidget {
        background-color: #2b2b2b;
        color: #dcdcdc;
        font-family: Arial;
        font-size: 11pt;
    }
    QLabel {
        color: #dcdcdc;
    }
    QLineEdit, QTextEdit, QListWidget, QTableWidget {
        background-color: #3c3f41;
        color: #bbbbbb;
        border: 1px solid #555555;
        selection-background-color: #54585a;
        selection-color: #ffffff;
    }
    QPushButton {
        background-color: #4a4d4f;
        color: #dcdcdc;
        border: 1px solid #555555;
        padding: 8px 12px;
        min-height: 1.5em; /* Ensure buttons are not too small */
    }
    QPushButton:hover {
        background-color: #5a5d5f;
    }
    QPushButton:pressed {
        background-color: #6a6d6f;
    }
    QPushButton:disabled {
        background-color: #3c3f41;
        color: #777777;
    }
    QCheckBox {
        color: #dcdcdc;
    }
    QCheckBox::indicator {
        width: 13px;
        height: 13px;
        background-color: #3c3f41;
        border: 1px solid #555;
    }
    QCheckBox::indicator:checked {
        background-color: #6897bb; /* Accent color */
    }
    QProgressBar {
        border: 1px solid #555555;
        text-align: center;
        color: #dcdcdc;
    }
    QProgressBar::chunk {
        background-color: #6897bb; /* Accent color */
    }
    QHeaderView::section {
        background-color: #3c3f41;
        color: #dcdcdc;
        padding: 4px;
        border: 1px solid #555555;
        font-weight: bold;
    }
    QTableWidget {
        gridline-color: #555555;
    }
    QSplitter::handle {
        background-color: #4a4d4f;
    }
    QSplitter::handle:horizontal {
        width: 5px;
    }
    QSplitter::handle:vertical {
        height: 5px;
    }
    QMenu {
        background-color: #3c3f41;
        color: #dcdcdc;
        border: 1px solid #555;
    }
    QMenu::item:selected {
        background-color: #54585a;
        color: #ffffff;
    }
    /* Special styles for success/failure */
    QTextEdit#successfulText {
        background-color: #274e13; /* Dark green */
    }
    QTextEdit#failedText {
        background-color: #592323; /* Dark red */
    }
    QLabel#successfulPostsLabel {
        color: #8fbc8f; /* Light green */
        font-weight: bold;
    }
    QLabel#failedPostsLabel, QLabel#tokenErrorsSpecificLabel, QLabel#bannedTokensSpecificLabel, QLabel#otherErrorsSpecificLabel {
        color: #f08080; /* Light red */
        font-weight: bold;
    }
    QLabel#lastPostDateLabel {
        color: #dda0dd; /* Light purple */
        font-weight: bold;
    }
    /* Style for separators */
    QFrame[frameShape="4"] {
        color: #555555;
        background-color: #555555;
    }
    QFrame[frameShape="5"] {
        color: #555555;
        background-color: #555555;
    }
"""
FONT_STYLE = QFont("Arial", 11)
HEADER_FONT = QFont("Arial", 12, QFont.Bold)

user_info_dict = {}
stop_flag = threading.Event()

# --- Helper Functions for UI separators ---
def create_horizontal_separator():
    """Создает горизонтальный разделитель"""
    separator = QFrame()
    separator.setFrameShape(QFrame.HLine)
    separator.setFrameShadow(QFrame.Sunken)
    return separator

def create_vertical_separator():
    """Создает вертикальный разделитель"""
    separator = QFrame()
    separator.setFrameShape(QFrame.VLine)
    separator.setFrameShadow(QFrame.Sunken)
    return separator

# --- Worker Signals ---
class WorkerSignals(QWidget):
    log_message = pyqtSignal(str)
    update_progress = pyqtSignal(int, int, int)
    successful_post = pyqtSignal(str, str)
    failed_post = pyqtSignal(str, str) # message, token
    enable_controls = pyqtSignal(bool)
    posting_finished = pyqtSignal()
    update_schedule_table = pyqtSignal(list)
    set_progress_max = pyqtSignal(int)
    increment_progress_bar = pyqtSignal()

# --- Helper Functions (Networking, VK API - mostly unchanged) ---
def natural_key(filename):
    basename = os.path.basename(filename)
    return [text for text in re.split(r'(\d+)', basename)]

def get_current_wifi_ssid():
    try:
        process = subprocess.Popen(['netsh', 'wlan', 'show', 'interfaces'],
                                     stdout=subprocess.PIPE, text=True,
                                     encoding='utf-8', errors='ignore',
                                     creationflags=subprocess.CREATE_NO_WINDOW)
        stdout, stderr = process.communicate(timeout=5)
        if process.returncode == 0:
            for line in stdout.splitlines():
                if "SSID" in line and "BSSID" not in line:
                    match = re.search(r":\s*(.+)", line)
                    if match:
                        ssid = match.group(1).strip()
                        if ssid: return ssid
            return "Нет подключения"
        return "Ошибка опроса"
    except Exception: return "Ошибка Wi-Fi"

def get_external_ip():
    try:
        response = requests.get('https://api.ipify.org', timeout=5)
        response.raise_for_status()
        return response.text.strip()
    except requests.RequestException: return "Недоступно"

def get_location_by_ip(ip):
    try:
        response = requests.get(f'https://ipinfo.io/{ip}/json', timeout=5)
        response.raise_for_status()
        data = response.json()
        city, region, country = data.get('city', 'Н/Д'), data.get('region', 'Н/Д'), data.get('country', 'Н/Д')
        return f"{city}, {region}, {country}"
    except requests.RequestException: return "Недоступно"

def get_user_info(token):
    url = 'https://api.vk.com/method/users.get'
    params = {'access_token': token, 'v': '5.131'}
    response = requests.get(url, params=params)
    data = response.json()
    if 'response' in data:
        user_info = data['response'][0]
        user_info_dict[token] = f"{user_info['first_name']} {user_info['last_name']}"
        return user_info['id'], user_info_dict[token]
    elif 'error' in data and data['error'].get('error_code') == 5:
        raise Exception(f"Ошибка авторизации: {data['error']['error_msg']}")
    else:
        raise Exception("Ошибка получения информации о пользователе: " + str(data.get('error', {})))

def upload_photo(token, photo_path):
    url = 'https://api.vk.com/method/photos.getWallUploadServer'
    params = {'access_token': token, 'v': '5.131'}
    response = requests.post(url, params=params)
    response.raise_for_status()
    upload_info = response.json()
    if 'response' not in upload_info:
        raise Exception("Ошибка получения сервера для загрузки фото: " + str(upload_info.get('error', {})))
    upload_url = upload_info['response']['upload_url']
    with open(photo_path, 'rb') as photo_file:
        files = {'photo': photo_file}
        upload_response = requests.post(upload_url, files=files)
        upload_response.raise_for_status()
        upload_data = upload_response.json()
    if 'photo' not in upload_data:
        raise Exception("Ошибка загрузки фото: " + str(upload_data))
    return upload_data

def save_photo(token, upload_data, user_id):
    url = 'https://api.vk.com/method/photos.saveWallPhoto'
    params = {
        'access_token': token, 'user_id': user_id, 'server': upload_data['server'],
        'photo': upload_data['photo'], 'hash': upload_data['hash'], 'v': '5.131'
    }
    response = requests.post(url, params=params)
    response.raise_for_status()
    data = response.json()
    if 'response' not in data:
        raise Exception("Ошибка сохранения фото: " + str(data.get('error', {})))
    return data

def upload_video(token, video_path):
    url = 'https://api.vk.com/method/video.save'
    params = {'access_token': token, 'v': '5.131', 'name': os.path.basename(video_path)}
    response = requests.post(url, params=params)
    response.raise_for_status()
    video_info = response.json()
    if 'response' not in video_info:
        raise Exception("Ошибка получения сервера для загрузки видео: " + str(video_info.get('error', {})))
    upload_url = video_info['response']['upload_url']
    with open(video_path, 'rb') as video_file:
        files = {'video_file': video_file}
        upload_response = requests.post(upload_url, files=files)
        upload_response.raise_for_status()
    if 'video_id' not in video_info['response']:
        raise Exception("Ошибка загрузки или сохранения видео: " + str(video_info.get('error', video_info)))
    return video_info['response']

def post_to_vk(token, user_id, message, attachments):
    url = 'https://api.vk.com/method/wall.post'
    params = {
        'access_token': token, 'owner_id': user_id, 'message': message,
        'attachments': attachments, 'v': '5.131'
    }
    response = requests.post(url, params=params)
    response.raise_for_status() 
    data = response.json()
    if 'error' in data: 
        raise Exception("Ошибка публикации поста: " + data['error']['error_msg'])
    return data

# --- Posting Logic Thread ---
class PostingWorker(QThread):
    def __init__(self, tokens_list, posting_schedule_list, autopost_flag, post_limit_val, photo_count_val, signals_emitter):
        super().__init__()
        self.tokens_list = tokens_list
        self.posting_schedule_list = posting_schedule_list
        self.autopost_flag = autopost_flag
        self.post_limit_val = post_limit_val
        self.photo_count_val = photo_count_val
        self.signals = signals_emitter
        self.current_successful_posts_count = 0
        self.current_failed_posts_count = 0
        self.final_failed_tokens_for_ui_copy = set()
        self.final_successful_tokens_for_ui_copy = set()

    def post_to_account(self, token, user_id_param, message, photos, videos):
        if stop_flag.is_set():
            return ("Публикация остановлена пользователем.", False, None)
        max_attempts = 3
        attempt = 0
        user_name = ""
        actual_user_id = user_id_param

        while attempt < max_attempts:
            if stop_flag.is_set():
                return ("Публикация остановлена пользователем.", False, None)
            attempt += 1
            try:
                if actual_user_id is None or attempt == 1:
                    actual_user_id, user_name = get_user_info(token)
                elif not user_name:
                     _, user_name = get_user_info(token)

                self.signals.log_message.emit(f"Публикация на аккаунте: {user_name if user_name else 'ID ' + str(actual_user_id)}... Попытка {attempt}\n")
                attachments = []
                for photo_idx, photo in enumerate(photos):
                    if stop_flag.is_set(): return ("Публикация остановлена.", False, None)
                    self.signals.log_message.emit(f"Загрузка фото {photo_idx+1}/{len(photos)}: {os.path.basename(photo)} на {user_name}...\n")
                    upload_data = upload_photo(token, photo)
                    saved_photo = save_photo(token, upload_data, actual_user_id)
                    if 'response' in saved_photo and saved_photo['response']:
                        attachments.append(f"photo{saved_photo['response'][0]['owner_id']}_{saved_photo['response'][0]['id']}")
                    else:
                        raise Exception(f"Не удалось сохранить фото {os.path.basename(photo)}. Ответ: {saved_photo}")

                for video_idx, video in enumerate(videos):
                    if stop_flag.is_set(): return ("Публикация остановлена.", False, None)
                    self.signals.log_message.emit(f"Загрузка видео {video_idx+1}/{len(videos)}: {os.path.basename(video)} на {user_name}...\n")
                    video_upload_response = upload_video(token, video)
                    video_owner_id = video_upload_response.get('owner_id', actual_user_id)
                    attachments.append(f"video{video_owner_id}_{video_upload_response['video_id']}")

                result = post_to_vk(token, actual_user_id, message, ','.join(attachments))
                post_id = result['response']['post_id']
                link = f"https://vk.com/wall{actual_user_id}_{post_id}"
                return (f"Пост успешно опубликован на аккаунте: {user_name}", link, actual_user_id)

            except Exception as e:
                error_text = str(e) 
                user_name_display = user_name if user_name else user_info_dict.get(token, token)
                if attempt >= max_attempts:
                    error_message = f"Ошибка на аккаунте {user_name_display}: {error_text} (после {max_attempts} попыток)\n"
                    return error_message, False, token 
                else:
                    self.signals.log_message.emit(f"Попытка {attempt} не удалась на {user_name_display}. Ошибка: {error_text}\n")
                    if "too many requests per second" in error_text.lower() or \
                       (isinstance(e, requests.exceptions.HTTPError) and e.response.status_code == 429) or \
                       ('error_code' in error_text and '6' in error_text):
                        self.signals.log_message.emit("Обнаружена ошибка 'too many requests'. Увеличиваю задержку...\n")
                        time.sleep(random.uniform(5,10))
                    else:
                        time.sleep(random.uniform(1,3))
        return (f"Не удалось опубликовать на {user_name_display} после {max_attempts} попыток.", False, token) 

    def move_uploaded_files(self, files):
        os.makedirs(UPLOADED_POSTS_DIR, exist_ok=True)
        for file_path in files:
            try:
                if os.path.exists(file_path):
                    shutil.move(file_path, os.path.join(UPLOADED_POSTS_DIR, os.path.basename(file_path)))
                    self.signals.log_message.emit(f"Файл перемещён: {os.path.basename(file_path)}\n")
                else:
                    self.signals.log_message.emit(f"Файл для перемещения не найден: {os.path.basename(file_path)}\n")
            except Exception as e:
                self.signals.log_message.emit(f"Ошибка при перемещении файла {os.path.basename(file_path)}: {e}\n")

    def run(self):
        stop_flag.clear()
        self.signals.enable_controls.emit(False)
        self.current_successful_posts_count = 0
        self.current_failed_posts_count = 0
        
        failed_tokens_this_run = set()
        successful_tokens_this_run = set()

        # If autopost is enabled, the worker itself generates the schedule.
        # If manual posting, it uses the schedule passed to it.
        current_posting_schedule = []
        if self.autopost_flag:
            if not os.path.exists(POSTS_DIR):
                self.signals.log_message.emit(f"Папка {POSTS_DIR} не найдена для автопостинга.\n")
                self.signals.posting_finished.emit()
                return
            all_photos = sorted([
                os.path.join(POSTS_DIR, f) for f in os.listdir(POSTS_DIR)
                if f.lower().endswith(('.jpg', '.jpeg', '.png'))
            ], key=natural_key)
            if not all_photos:
                self.signals.log_message.emit("В папке 'посты' нет фотографий для автопостинга.\n")
                self.signals.posting_finished.emit()
                return
            
            # Use post_limit_val and photo_count_val passed to constructor
            for i in range(0, min(self.post_limit_val * self.photo_count_val, len(all_photos)), self.photo_count_val):
                photos_for_post = all_photos[i:i + self.photo_count_val]
                current_posting_schedule.append({'text': '', 'photos': photos_for_post, 'videos': []})
            self.signals.log_message.emit(f"Автопостинг: сформировано {len(current_posting_schedule)} постов.\n")
            # Update UI table only if this worker is the primary one (not a retry, or handle differently)
            # For simplicity, retries might not update the main table display with this generated schedule
            # if the original call was a manual one. The main app handles the table for manual.
            if self.signals.update_schedule_table: # Check if signal is connected
                 self.signals.update_schedule_table.emit(list(current_posting_schedule))
        else: # Manual mode, use the schedule passed in
            current_posting_schedule = self.posting_schedule_list

        if not self.tokens_list:
            self.signals.log_message.emit("Нет доступных токенов для публикации.\n")
            self.signals.posting_finished.emit()
            return

        if not current_posting_schedule: # Check the effective schedule
            self.signals.log_message.emit("Нет наборов публикаций для выполнения.\n")
            self.signals.posting_finished.emit()
            return

        total_posts_to_make = len(current_posting_schedule) * len(self.tokens_list)
        self.signals.set_progress_max.emit(total_posts_to_make)
        self.signals.update_progress.emit(0, 0, total_posts_to_make)

        for entry in current_posting_schedule:
            entry['photos'] = sorted(entry.get('photos', []), key=natural_key)
            entry['videos'] = sorted(entry.get('videos', []), key=natural_key)
        self.signals.log_message.emit("Файлы в наборах публикаций отсортированы.\n")

        for entry_idx, entry in enumerate(current_posting_schedule):
            if stop_flag.is_set(): break

            message = entry['text']
            photos_to_post = entry['photos']
            videos_to_post = entry['videos']
            successful_posts_for_this_entry_on_tokens = 0

            valid_tokens_for_this_entry = [token for token in self.tokens_list if token not in failed_tokens_this_run]

            if not valid_tokens_for_this_entry:
                self.signals.log_message.emit(f"Для публикации №{entry_idx + 1} нет доступных (не сфейлившихся ранее) токенов.\n")
                self.current_failed_posts_count += len(self.tokens_list) - len(failed_tokens_this_run)
                self.signals.update_progress.emit(self.current_successful_posts_count, self.current_failed_posts_count, total_posts_to_make)
                for _ in range(len(self.tokens_list) - len(failed_tokens_this_run)):
                     self.signals.increment_progress_bar.emit()
                continue

            self.signals.log_message.emit(f"--- Начало публикации набора №{entry_idx + 1} (Текст: '{message[:30].replace(os.linesep, ' ')}...', Фото: {len(photos_to_post)}, Видео: {len(videos_to_post)}) ---\n")
            
            with ThreadPoolExecutor(max_workers=min(10, len(valid_tokens_for_this_entry))) as executor:
                future_to_token = {
                    executor.submit(self.post_to_account, token, None, message, photos_to_post, videos_to_post): token
                    for token in valid_tokens_for_this_entry
                }

                for future in as_completed(future_to_token):
                    if stop_flag.is_set():
                        for f_in_loop in future_to_token:
                            if not f_in_loop.done(): f_in_loop.cancel()
                        break
                    
                    token_from_future = future_to_token[future]
                    try:
                        result = future.result()
                        if len(result) == 3:
                            status_message, outcome_is_link_or_false, problematic_item = result 
                            
                            if outcome_is_link_or_false == False :
                                self.current_failed_posts_count += 1
                                self.signals.failed_post.emit(status_message, problematic_item) 
                                if problematic_item:
                                    failed_tokens_this_run.add(problematic_item)
                                    self.final_failed_tokens_for_ui_copy.add(problematic_item)
                            else:
                                self.current_successful_posts_count += 1
                                successful_posts_for_this_entry_on_tokens +=1
                                successful_tokens_this_run.add(token_from_future) 
                                self.final_successful_tokens_for_ui_copy.add(token_from_future)
                                self.signals.successful_post.emit(status_message, outcome_is_link_or_false)
                        else: 
                            self.current_failed_posts_count += 1
                            display_name = user_info_dict.get(token_from_future, token_from_future)
                            self.signals.failed_post.emit(f"Неизвестная ошибка обработки для токена: {display_name}.\n", token_from_future)
                            failed_tokens_this_run.add(token_from_future)
                            self.final_failed_tokens_for_ui_copy.add(token_from_future)

                    except Exception as exc: 
                        self.current_failed_posts_count += 1
                        display_name = user_info_dict.get(token_from_future, token_from_future)
                        error_msg = f"Критическая ошибка для {display_name} при получении результата: {exc}\n"
                        self.signals.failed_post.emit(error_msg, token_from_future)
                        failed_tokens_this_run.add(token_from_future)
                        self.final_failed_tokens_for_ui_copy.add(token_from_future)
                    
                    self.signals.increment_progress_bar.emit()
                    self.signals.update_progress.emit(self.current_successful_posts_count, self.current_failed_posts_count, total_posts_to_make)

            if not stop_flag.is_set():
                # Only move files if autoposting AND this worker generated its own schedule
                if self.autopost_flag and (photos_to_post or videos_to_post):
                    log_msg_prefix = ""
                    if not valid_tokens_for_this_entry:
                        log_msg_prefix = f"Набор №{entry_idx + 1}: не было валидных токенов."
                    elif successful_posts_for_this_entry_on_tokens == len(valid_tokens_for_this_entry):
                        log_msg_prefix = f"Набор №{entry_idx + 1} успешно опубликован на всех ({successful_posts_for_this_entry_on_tokens}) аккаунтах."
                    elif successful_posts_for_this_entry_on_tokens > 0:
                            log_msg_prefix = f"Набор №{entry_idx + 1} опубликован на {successful_posts_for_this_entry_on_tokens} из {len(valid_tokens_for_this_entry)} аккаунтов."
                    else:
                            log_msg_prefix = f"Набор №{entry_idx + 1} не опубликован ни на одном из {len(valid_tokens_for_this_entry)} аккаунтов."
                    
                    self.signals.log_message.emit(f"{log_msg_prefix} Перемещение файлов...\n")
                    self.move_uploaded_files(photos_to_post + videos_to_post)
                # Text-only post logging (applies to both auto and manual if files are empty)
                elif not (photos_to_post or videos_to_post):
                    if not valid_tokens_for_this_entry:
                            self.signals.log_message.emit(f"Набор №{entry_idx + 1} (текстовый): не было валидных токенов для публикации.\n")
                    elif successful_posts_for_this_entry_on_tokens == len(valid_tokens_for_this_entry) and len(valid_tokens_for_this_entry) > 0:
                        self.signals.log_message.emit(f"Набор №{entry_idx + 1} (текстовый) успешно опубликован на всех ({len(valid_tokens_for_this_entry)}) доступных аккаунтах.\n")
                    elif len(valid_tokens_for_this_entry) > 0 :
                            self.signals.log_message.emit(f"Набор №{entry_idx + 1} (текстовый) опубликован не на всех ({successful_posts_for_this_entry_on_tokens}/{len(valid_tokens_for_this_entry)}) аккаунтах.\n")
            else: # Stop flag was set
                if self.autopost_flag and (photos_to_post or videos_to_post):
                    self.signals.log_message.emit(f"Обработка набора №{entry_idx + 1} была прервана. Файлы не будут перемещены.\n")

            if stop_flag.is_set(): break
        
        if failed_tokens_this_run:
            self.write_failed_tokens(failed_tokens_this_run)

        final_message = "Публикация завершена.\n" if not stop_flag.is_set() else "Процесс был остановлен пользователем.\n"
        self.signals.log_message.emit(final_message)
        self.signals.update_progress.emit(self.current_successful_posts_count, self.current_failed_posts_count, total_posts_to_make)
        self.signals.posting_finished.emit()

    def write_failed_tokens(self, failed_tokens_set_param):
        separator = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " ------------------------\n"
        try:
            with open(FAILED_TOKENS_FILE_PATH, 'a', encoding='utf-8') as file:
                file.write(separator)
                for token in failed_tokens_set_param:
                    file.write(token + '\n')
            self.signals.log_message.emit(f"Токены неуспешных публикаций записаны в {FAILED_TOKENS_FILE_PATH}\n")
        except Exception as e:
            self.signals.log_message.emit(f"Ошибка записи токенов неуспешных публикаций: {e}\n")
            
    def get_final_failed_tokens(self):
        return self.final_failed_tokens_for_ui_copy

    def get_final_successful_tokens(self):
        return self.final_successful_tokens_for_ui_copy

# --- Add Posting Set Dialog ---
class AddPostingSetDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Добавить набор публикации")
        self.setGeometry(300, 300, 700, 550)
        self.setModal(True)
        self.posting_schedule_files = []
        self.initUI()
        self.center()

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def initUI(self):
        layout = QVBoxLayout(self)
        title_label = QLabel("Новый набор публикации")
        title_label.setFont(HEADER_FONT)
        layout.addWidget(title_label)

        # Разделитель после заголовка
        layout.addWidget(create_horizontal_separator())

        files_label_text = QLabel("Файлы (Фото и Видео): Перетащите файлы сюда или используйте кнопки.")
        layout.addWidget(files_label_text)
        files_layout = QHBoxLayout()
        self.files_listbox = QListWidget()
        self.files_listbox.setSelectionMode(QListWidget.ExtendedSelection)
        self.files_listbox.setAcceptDrops(True)
        self.files_listbox.dragEnterEvent = self.dragEnterEventFiles
        self.files_listbox.dragMoveEvent = self.dragMoveEventFiles
        self.files_listbox.dropEvent = self.dropEventFiles
        files_layout.addWidget(self.files_listbox, 1)
        
        # Вертикальный разделитель между списком файлов и кнопками
        files_layout.addWidget(create_vertical_separator())
        
        files_buttons_layout = QVBoxLayout()
        add_files_button = QPushButton("+"); add_files_button.setFixedWidth(40)
        add_files_button.clicked.connect(self.add_files_dialog)
        files_buttons_layout.addWidget(add_files_button)
        remove_files_button = QPushButton("-"); remove_files_button.setFixedWidth(40)
        remove_files_button.clicked.connect(self.remove_selected_files_dialog)
        files_buttons_layout.addWidget(remove_files_button)
        files_buttons_layout.addStretch()
        files_layout.addLayout(files_buttons_layout)
        layout.addLayout(files_layout)

        # Разделитель между файлами и текстом
        layout.addWidget(create_horizontal_separator())

        text_label = QLabel("Текст сообщения:")
        layout.addWidget(text_label)
        message_layout = QHBoxLayout()
        self.message_entry = QTextEdit(); self.message_entry.setFixedHeight(100)
        message_layout.addWidget(self.message_entry, 1)
        
        # Вертикальный разделитель между текстом и кнопкой
        message_layout.addWidget(create_vertical_separator())
        
        paste_button_child = QPushButton("Вставить")
        paste_button_child.clicked.connect(lambda: self.message_entry.paste())
        message_btn_vlayout = QVBoxLayout(); message_btn_vlayout.addWidget(paste_button_child); message_btn_vlayout.addStretch()
        message_layout.addLayout(message_btn_vlayout)
        layout.addLayout(message_layout)

        # Разделитель перед кнопками
        layout.addWidget(create_horizontal_separator())

        buttons_frame_layout = QHBoxLayout(); buttons_frame_layout.addStretch()
        save_and_new_button = QPushButton("Сохранить и Новый")
        save_and_new_button.clicked.connect(lambda: self.save_entry_dialog(close_after=False))
        buttons_frame_layout.addWidget(save_and_new_button)
        save_button = QPushButton("Сохранить и Закрыть")
        save_button.clicked.connect(lambda: self.save_entry_dialog(close_after=True))
        buttons_frame_layout.addWidget(save_button)
        cancel_button = QPushButton("Отмена")
        cancel_button.clicked.connect(self.reject)
        buttons_frame_layout.addWidget(cancel_button)
        buttons_frame_layout.addStretch()
        layout.addLayout(buttons_frame_layout)
        self.setLayout(layout)

    def dragEnterEventFiles(self, event):
        if event.mimeData().hasUrls(): event.acceptProposedAction()
        else: event.ignore()

    def dragMoveEventFiles(self, event):
        if event.mimeData().hasUrls(): event.acceptProposedAction()
        else: event.ignore()

    def dropEventFiles(self, event):
        files = [url.toLocalFile() for url in event.mimeData().urls()]
        valid_files_to_add = [
            f for f in files
            if os.path.exists(f) and f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.mp4', '.avi', '.mov', '.mkv'))
        ]
        newly_added_basenames = []
        for file_path in valid_files_to_add:
            if file_path not in self.posting_schedule_files:
                self.posting_schedule_files.append(file_path)
                self.files_listbox.addItem(QListWidgetItem(os.path.basename(file_path)))
                newly_added_basenames.append(os.path.basename(file_path))
        if newly_added_basenames and self.parent():
             self.parent().log_message_slot(f"Добавлено файлов через DnD: {', '.join(newly_added_basenames)}\n")

    def add_files_dialog(self):
        selected_files, _ = QFileDialog.getOpenFileNames(
            self, "Выберите фото и/или видео", "",
            "Медиа файлы (*.jpg *.jpeg *.png *.gif *.mp4 *.avi *.mov *.mkv);;Все файлы (*.*)"
        )
        newly_added_basenames = []
        for file_path in selected_files:
            if file_path not in self.posting_schedule_files:
                self.posting_schedule_files.append(file_path)
                self.files_listbox.addItem(QListWidgetItem(os.path.basename(file_path)))
                newly_added_basenames.append(os.path.basename(file_path))
        if newly_added_basenames and self.parent():
             self.parent().log_message_slot(f"Добавлено файлов через диалог: {', '.join(newly_added_basenames)}\n")

    def remove_selected_files_dialog(self):
        selected_items = self.files_listbox.selectedItems()
        if not selected_items: return
        removed_basenames = []
        for item in reversed(selected_items):
            row = self.files_listbox.row(item)
            base_name = os.path.basename(self.posting_schedule_files[row])
            removed_basenames.append(base_name)
            del self.posting_schedule_files[row]
            self.files_listbox.takeItem(row)
        if removed_basenames and self.parent():
            self.parent().log_message_slot(f"Удалены выбранные файлы: {', '.join(reversed(removed_basenames))}\n")

    def save_entry_dialog(self, close_after=False):
        text = self.message_entry.toPlainText().strip()
        if not self.posting_schedule_files:
            QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один файл.")
            return False
        if not text:
            reply = QMessageBox.question(self, "Подтверждение", "Сообщение пустое. Продолжить?",
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.No: return False
        photos_paths = [f for f in self.posting_schedule_files if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))]
        videos_paths = [f for f in self.posting_schedule_files if f.lower().endswith(('.mp4', '.avi', '.mov', '.mkv'))]
        if self.parent():
            self.parent().add_posting_set_data({'text': text, 'photos': photos_paths, 'videos': videos_paths})
            self.parent().log_message_slot("Новый набор публикации сохранен.\n")
        self.message_entry.clear()
        self.files_listbox.clear()
        self.posting_schedule_files.clear()
        if close_after: self.accept()
        return True

# --- Main Application Window ---
class VKAutoPosterApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Автопостинг ВКонтакте")
        self.setGeometry(100, 100, 1250, 850)
        self.tokens = []
        self.posting_schedule = []
        self.auto_scroll_log = True
        self.worker_signals = WorkerSignals()
        self.posting_thread = None
        self.final_failed_tokens_for_ui_copy = set()
        self.final_successful_tokens_for_ui_copy = set()

        self.token_error_specific_count = 0
        self.banned_token_specific_count = 0
        self.other_error_specific_count = 0 

        self.auth_error_tokens = set()
        self.banned_error_tokens = set()
        self.other_error_tokens = set()

        self.elapsed_time_label = QLabel("Время: 00:00:00")
        self.runtime_qtimer = QTimer(self)
        self.runtime_qtimer.timeout.connect(self.update_runtime_display)
        self.posting_start_time = None
        self.elapsed_seconds = 0

        self.initUI()
        self.center()  # Центрирование окна
        self.load_settings()
        self.update_add_schedule_button_state()
        self.update_buttons_on_token_load()
        self.update_retry_button_state() # Initial state

        self.info_update_timer = QTimer(self)
        self.info_update_timer.timeout.connect(self.update_info_display_slot)
        self.info_update_timer.start(15000)
        self.update_info_display_slot()

        # Загружаем дату последней публикации
        self.load_last_post_date()

        self.worker_signals.log_message.connect(self.log_message_slot)
        self.worker_signals.update_progress.connect(self.update_progress_labels_slot)
        self.worker_signals.successful_post.connect(self.successful_post_slot)
        self.worker_signals.failed_post.connect(self.failed_post_slot)
        self.worker_signals.enable_controls.connect(self.toggle_posting_buttons_slot)
        self.worker_signals.posting_finished.connect(self.on_posting_finished)
        self.worker_signals.update_schedule_table.connect(self.receive_schedule_update_from_worker)
        self.worker_signals.set_progress_max.connect(self.progress_bar.setMaximum)
        self.worker_signals.increment_progress_bar.connect(self.increment_progress_bar_slot)

    def center(self):
        """Центрирование окна на экране"""
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def load_last_post_date(self):
        """Загрузка даты последней публикации"""
        if os.path.exists(LAST_POST_DATE_FILE_PATH):
            try:
                with open(LAST_POST_DATE_FILE_PATH, 'r', encoding='utf-8') as f:
                    last_date = f.read().strip()
                    if last_date:
                        self.last_post_date_label.setText(f"Последняя публикация: {last_date}")
                    else:
                        self.last_post_date_label.setText("Последняя публикация: Нет данных")
            except Exception as e:
                self.log_message_slot(f"Ошибка загрузки даты последней публикации: {e}\n")
                self.last_post_date_label.setText("Последняя публикация: Ошибка загрузки")
        else:
            self.last_post_date_label.setText("Последняя публикация: Нет данных")

    def save_last_post_date(self):
        """Сохранение даты последней публикации"""
        current_datetime = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
        try:
            with open(LAST_POST_DATE_FILE_PATH, 'w', encoding='utf-8') as f:
                f.write(current_datetime)
            self.last_post_date_label.setText(f"Последняя публикация: {current_datetime}")
        except Exception as e:
            self.log_message_slot(f"Ошибка сохранения даты последней публикации: {e}\n")

    def initUI(self):
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # Секция прогресс-бара
        progress_outer_layout = QVBoxLayout()
        self.progress_bar = QProgressBar(); self.progress_bar.setValue(0); self.progress_bar.setTextVisible(False)
        progress_outer_layout.addWidget(self.progress_bar)
        
        labels_info_layout = QHBoxLayout() 
        self.progress_label = QLabel("Осталось постов: 0") 
        labels_info_layout.addWidget(self.progress_label)
        labels_info_layout.addStretch()
        self.elapsed_time_label.setAlignment(Qt.AlignRight) 
        labels_info_layout.addWidget(self.elapsed_time_label)
        progress_outer_layout.addLayout(labels_info_layout)
        
        main_layout.addLayout(progress_outer_layout)

        # Разделитель после прогресс-бара
        main_layout.addWidget(create_horizontal_separator())

        # Секция информационных меток
        info_frame_layout = QHBoxLayout()
        self.successful_posts_label = QLabel("Успешные публикации: 0"); self.successful_posts_label.setObjectName("successfulPostsLabel")
        self.successful_posts_label.setToolTip("Нажмите, чтобы скопировать успешные токены")
        self.successful_posts_label.mousePressEvent = lambda e: self.copy_tokens_to_clipboard_handler(True)
        info_frame_layout.addWidget(self.successful_posts_label)
        
        # Вертикальный разделитель между метками
        info_frame_layout.addWidget(create_vertical_separator())
        
        self.failed_posts_label = QLabel("Неуспешные публикации: 0"); self.failed_posts_label.setObjectName("failedPostsLabel")
        self.failed_posts_label.setToolTip("Нажмите, чтобы скопировать все неуспешные токены")
        self.failed_posts_label.mousePressEvent = lambda e: self.copy_tokens_to_clipboard_handler(False)
        info_frame_layout.addWidget(self.failed_posts_label)
        
        info_frame_layout.addWidget(create_vertical_separator())
        
        # Добавляем метку для отображения даты последней публикации
        self.last_post_date_label = QLabel("Последняя публикация: Нет данных")
        self.last_post_date_label.setObjectName("lastPostDateLabel")
        self.last_post_date_label.setToolTip("Дата и время последней успешной публикации")
        info_frame_layout.addWidget(self.last_post_date_label)
        
        info_frame_layout.addStretch(1)
        
        # Вертикальный разделитель перед сетевой информацией
        info_frame_layout.addWidget(create_vertical_separator())
        
        self.wifi_status_label = QLabel("Wi-Fi: Сканирование...")
        info_frame_layout.addWidget(self.wifi_status_label)
        
        info_frame_layout.addWidget(create_vertical_separator())
        
        self.ip_label = QLabel("IP: Сканирование...")
        info_frame_layout.addWidget(self.ip_label)
        
        info_frame_layout.addWidget(create_vertical_separator())
        
        self.location_label = QLabel("Локация: Сканирование...")
        info_frame_layout.addWidget(self.location_label)
        main_layout.addLayout(info_frame_layout)

        # Разделитель после информационных меток
        main_layout.addWidget(create_horizontal_separator())

        # Секция настроек автопостинга
        autopost_settings_layout = QHBoxLayout()
        self.autopost_check = QCheckBox("Автопубликация")
        self.autopost_check.stateChanged.connect(self.save_autopost_state_setting)
        self.autopost_check.stateChanged.connect(self.update_add_schedule_button_state)
        autopost_settings_layout.addWidget(self.autopost_check)
        
        autopost_settings_layout.addWidget(create_vertical_separator())
        
        autopost_settings_layout.addWidget(QLabel("Лимит постов:"))
        self.post_limit_entry = QLineEdit("1"); self.post_limit_entry.setFixedWidth(50)
        self.post_limit_entry.textChanged.connect(self.save_post_limit_setting)
        autopost_settings_layout.addWidget(self.post_limit_entry)
        
        autopost_settings_layout.addWidget(create_vertical_separator())
        
        autopost_settings_layout.addWidget(QLabel("Фотографий на пост:"))
        self.photo_count_entry = QLineEdit("1"); self.photo_count_entry.setFixedWidth(50)
        self.photo_count_entry.textChanged.connect(self.save_photo_count_setting)
        autopost_settings_layout.addWidget(self.photo_count_entry)
        autopost_settings_layout.addStretch()
        main_layout.addLayout(autopost_settings_layout)

        # Разделитель после настроек автопостинга
        main_layout.addWidget(create_horizontal_separator())

        # Секция управления аккаунтами
        account_mgmt_layout = QHBoxLayout()
        self.account_button = QPushButton("Указать аккаунты")
        self.account_button.clicked.connect(self.load_tokens_ui_update)
        account_mgmt_layout.addWidget(self.account_button)
        
        account_mgmt_layout.addWidget(create_vertical_separator())
        
        self.tokens_label = QLabel("Токенов: 0")
        account_mgmt_layout.addWidget(self.tokens_label)
        account_mgmt_layout.addStretch()
        main_layout.addLayout(account_mgmt_layout)

        # Разделитель после управления аккаунтами
        main_layout.addWidget(create_horizontal_separator())

        # Секция кнопок управления
        controls_layout = QHBoxLayout(); controls_layout.addStretch()
        self.post_button = QPushButton("Выложить пост")
        self.post_button.clicked.connect(self.submit_post_handler)
        controls_layout.addWidget(self.post_button)
        
        controls_layout.addWidget(create_vertical_separator())
        
        self.stop_button = QPushButton("Остановить"); self.stop_button.setEnabled(False)
        self.stop_button.clicked.connect(self.stop_posting_handler)
        controls_layout.addWidget(self.stop_button)
        
        controls_layout.addWidget(create_vertical_separator())
        
        self.retry_other_errors_button = QPushButton("Повторить для 'Прочих ошибок'")
        self.retry_other_errors_button.clicked.connect(self.retry_other_errors_handler)
        self.retry_other_errors_button.setEnabled(False) # Initially disabled
        controls_layout.addWidget(self.retry_other_errors_button)

        controls_layout.addWidget(create_vertical_separator())

        self.add_schedule_button = QPushButton("Добавить набор публикаций")
        self.add_schedule_button.clicked.connect(self.open_add_posting_set_dialog)
        controls_layout.addWidget(self.add_schedule_button)
        
        controls_layout.addWidget(create_vertical_separator())
        
        self.remove_schedule_button = QPushButton("Удалить выбранные")
        self.remove_schedule_button.clicked.connect(self.remove_selected_posting_sets)
        controls_layout.addWidget(self.remove_schedule_button)
        controls_layout.addStretch()
        main_layout.addLayout(controls_layout)

        # Разделитель перед основными панелями
        main_layout.addWidget(create_horizontal_separator())

        # Основные панели (таблица и лог)
        splitter_schedule_log = QSplitter(Qt.Horizontal)
        schedule_table_container = QWidget(); schedule_table_layout = QVBoxLayout(schedule_table_container)
        schedule_table_layout.addWidget(QLabel("План публикаций:"))
        self.posting_schedule_table = QTableWidget(); self.posting_schedule_table.setColumnCount(4)
        self.posting_schedule_table.setHorizontalHeaderLabels(["№", "Фото", "Видео", "Текст"])
        self.posting_schedule_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
        self.posting_schedule_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.posting_schedule_table.setSelectionBehavior(QTableWidget.SelectRows)
        self.posting_schedule_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.posting_schedule_table.customContextMenuRequested.connect(self.show_table_context_menu)
        schedule_table_layout.addWidget(self.posting_schedule_table)
        splitter_schedule_log.addWidget(schedule_table_container)
        log_output_container = QWidget(); log_output_layout = QVBoxLayout(log_output_container)
        log_output_layout.addWidget(QLabel("Лог процесса:"))
        self.output_text = QTextEdit(); self.output_text.setReadOnly(True)
        self.output_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.output_text.customContextMenuRequested.connect(lambda pos: self.show_text_edit_context_menu(pos, self.output_text))
        log_output_layout.addWidget(self.output_text)
        splitter_schedule_log.addWidget(log_output_container)
        splitter_schedule_log.setSizes([400, 600])
        main_layout.addWidget(splitter_schedule_log, 1)

        # Панели результатов
        splitter_results = QSplitter(Qt.Horizontal)
        successful_container = QWidget(); successful_layout = QVBoxLayout(successful_container)
        successful_layout.addWidget(QLabel("Успешные публикации:"))
        self.successful_text = QTextEdit(); self.successful_text.setReadOnly(True); self.successful_text.setObjectName("successfulText")
        self.successful_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.successful_text.customContextMenuRequested.connect(lambda pos: self.show_text_edit_context_menu(pos, self.successful_text))
        successful_layout.addWidget(self.successful_text)
        splitter_results.addWidget(successful_container)
        
        failed_container = QWidget(); failed_layout = QVBoxLayout(failed_container)
        failed_layout.addWidget(QLabel("Неуспешные публикации:"))

        # Специфичные счетчики ошибок с разделителями
        specific_failed_counts_layout = QHBoxLayout()
        self.token_errors_specific_label = QLabel("Ошибка авторизации (токен): 0")
        self.token_errors_specific_label.setObjectName("tokenErrorsSpecificLabel") 
        self.token_errors_specific_label.setToolTip("Нажмите, чтобы скопировать токены с ошибкой авторизации")
        self.token_errors_specific_label.mousePressEvent = lambda event, cat="auth": self.copy_categorized_tokens_handler(cat)
        self.token_errors_specific_label.setCursor(QCursor(Qt.PointingHandCursor))
        specific_failed_counts_layout.addWidget(self.token_errors_specific_label)

        specific_failed_counts_layout.addWidget(create_vertical_separator())

        self.banned_tokens_specific_label = QLabel("Аккаунт заблокирован: 0")
        self.banned_tokens_specific_label.setObjectName("bannedTokensSpecificLabel") 
        self.banned_tokens_specific_label.setToolTip("Нажмите, чтобы скопировать заблокированные токены")
        self.banned_tokens_specific_label.mousePressEvent = lambda event, cat="banned": self.copy_categorized_tokens_handler(cat)
        self.banned_tokens_specific_label.setCursor(QCursor(Qt.PointingHandCursor))
        specific_failed_counts_layout.addWidget(self.banned_tokens_specific_label)
        
        specific_failed_counts_layout.addWidget(create_vertical_separator())
        
        self.other_errors_specific_label = QLabel("Прочие ошибки: 0") 
        self.other_errors_specific_label.setObjectName("otherErrorsSpecificLabel")
        self.other_errors_specific_label.setToolTip("Нажмите, чтобы скопировать токены с прочими ошибками")
        self.other_errors_specific_label.mousePressEvent = lambda event, cat="other": self.copy_categorized_tokens_handler(cat)
        self.other_errors_specific_label.setCursor(QCursor(Qt.PointingHandCursor))
        specific_failed_counts_layout.addWidget(self.other_errors_specific_label)

        specific_failed_counts_layout.addStretch() 
        failed_layout.addLayout(specific_failed_counts_layout)

        # Разделитель между счетчиками и текстом ошибок
        failed_layout.addWidget(create_horizontal_separator())

        self.failed_text = QTextEdit(); self.failed_text.setReadOnly(True); self.failed_text.setObjectName("failedText")
        self.failed_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.failed_text.customContextMenuRequested.connect(lambda pos: self.show_text_edit_context_menu(pos, self.failed_text))
        failed_layout.addWidget(self.failed_text)
        splitter_results.addWidget(failed_container)
        splitter_results.setSizes([500,500])
        main_layout.addWidget(splitter_results, 1)
        self.setFont(FONT_STYLE)

    def update_runtime_display(self):
        self.elapsed_seconds += 1
        hours = self.elapsed_seconds // 3600
        minutes = (self.elapsed_seconds % 3600) // 60
        seconds = self.elapsed_seconds % 60
        self.elapsed_time_label.setText(f"Время: {hours:02}:{minutes:02}:{seconds:02}")

    def copy_tokens_to_clipboard_handler(self, successful=True):
        tokens_set = self.final_successful_tokens_for_ui_copy if successful else self.final_failed_tokens_for_ui_copy
        type_name = "успешных" if successful else "всех неуспешных"
        if not tokens_set:
            self.log_message_slot(f"Нет {type_name} токенов для копирования в буфер.\n")
            return
        try:
            tokens_str = "\n".join(sorted(list(tokens_set)))
            QApplication.clipboard().setText(tokens_str)
            self.log_message_slot(f"{type_name.capitalize()} токены ({len(tokens_set)} шт.) скопированы в буфер обмена.\n")
        except Exception as e:
            self.log_message_slot(f"Ошибка копирования токенов ({type_name}) в буфер: {e}\n")
            
    def copy_categorized_tokens_handler(self, category_key):
        tokens_to_copy = set()
        category_name_log = "" 

        if category_key == "auth":
            tokens_to_copy = self.auth_error_tokens
            category_name_log = "с ошибкой авторизации (токен)"
        elif category_key == "banned":
            tokens_to_copy = self.banned_error_tokens
            category_name_log = "заблокированных аккаунтов"
        elif category_key == "other":
            tokens_to_copy = self.other_error_tokens
            category_name_log = "с прочими ошибками"
        else:
            self.log_message_slot(f"Неизвестная категория токенов для копирования: {category_key}\n")
            return

        if not tokens_to_copy:
            self.log_message_slot(f"Нет токенов ({category_name_log}) для копирования.\n")
            return
        try:
            tokens_str = "\n".join(sorted(list(tokens_to_copy)))
            QApplication.clipboard().setText(tokens_str)
            self.log_message_slot(f"Токены ({len(tokens_to_copy)} шт.) {category_name_log} скопированы в буфер обмена.\n")
        except Exception as e:
            self.log_message_slot(f"Ошибка копирования токенов ({category_name_log}) в буфер: {e}\n")

    def receive_schedule_update_from_worker(self, new_schedule):
        self.posting_schedule = new_schedule
        self.update_posting_schedule_table_display()

    def increment_progress_bar_slot(self):
        self.progress_bar.setValue(self.progress_bar.value() + 1)

    def show_text_edit_context_menu(self, position, text_edit_widget):
        menu = QMenu(); copy_action = menu.addAction("Копировать"); select_all_action = menu.addAction("Выбрать все")
        menu.addSeparator()
        if text_edit_widget == self.output_text:
            auto_scroll_action = QAction("Включить автопрокрутку", self, checkable=True)
            auto_scroll_action.setChecked(self.auto_scroll_log)
            auto_scroll_action.triggered.connect(self.toggle_auto_scroll_log)
            menu.addAction(auto_scroll_action)
        action = menu.exec_(text_edit_widget.mapToGlobal(position))
        if action == copy_action: text_edit_widget.copy()
        elif action == select_all_action: text_edit_widget.selectAll()

    def toggle_auto_scroll_log(self):
        self.auto_scroll_log = not self.auto_scroll_log
        self.log_message_slot(f"Автопрокрутка лога {'включена' if self.auto_scroll_log else 'отключена'}.\n")

    def show_table_context_menu(self, position):
        menu = QMenu(); select_all_action = menu.addAction("Выбрать все"); remove_action = menu.addAction("Удалить выбранные")
        if not self.posting_schedule_table.selectedItems(): remove_action.setEnabled(False)
        action = menu.exec_(self.posting_schedule_table.viewport().mapToGlobal(position))
        if action == select_all_action: self.posting_schedule_table.selectAll()
        elif action == remove_action: self.remove_selected_posting_sets()

    def update_info_display_slot(self):
        threading.Thread(target=self._fetch_network_info, daemon=True).start()

    def _fetch_network_info(self):
        ssid = get_current_wifi_ssid(); ip = get_external_ip()
        location = "Недоступно"; 
        if ip != "Недоступно": location = get_location_by_ip(ip)
        self.wifi_status_label.setText(f"Wi-Fi: {ssid}"); self.ip_label.setText(f"IP: {ip}"); self.location_label.setText(f"Локация: {location}")

    def load_settings(self):
        autopost_enabled = False
        if os.path.exists(AUTOPOST_STATE_FILE_PATH):
            try:
                with open(AUTOPOST_STATE_FILE_PATH, 'r', encoding='utf-8') as f: autopost_enabled = f.read().strip().lower() == 'true'
            except Exception as e: self.log_message_slot(f"Ошибка загрузки состояния автопостинга: {e}\n")
        self.autopost_check.setChecked(autopost_enabled)
        if os.path.exists(POST_LIMIT_FILE_PATH):
            try:
                with open(POST_LIMIT_FILE_PATH, 'r', encoding='utf-8') as f: self.post_limit_entry.setText(f.read().strip() or "1")
            except Exception as e: self.log_message_slot(f"Ошибка загрузки лимита постов: {e}\n")
        else: self.post_limit_entry.setText("1")
        if os.path.exists(PHOTO_COUNT_FILE_PATH):
            try:
                with open(PHOTO_COUNT_FILE_PATH, 'r', encoding='utf-8') as f: self.photo_count_entry.setText(f.read().strip() or "1")
            except Exception as e: self.log_message_slot(f"Ошибка загрузки кол-ва фото: {e}\n")
        else: self.photo_count_entry.setText("1")
        self.load_tokens_ui_update()

    def save_setting_to_file(self, file_path, value_to_save, error_msg_prefix):
        try:
            with open(file_path, 'w', encoding='utf-8') as f: f.write(str(value_to_save))
        except Exception as e: self.log_message_slot(f"{error_msg_prefix}: {e}\n")

    def save_autopost_state_setting(self): self.save_setting_to_file(AUTOPOST_STATE_FILE_PATH, self.autopost_check.isChecked(), "Ошибка сохранения состояния автопостинга")
    def save_post_limit_setting(self): self.save_setting_to_file(POST_LIMIT_FILE_PATH, self.post_limit_entry.text(), "Ошибка сохранения лимита постов")
    def save_photo_count_setting(self): self.save_setting_to_file(PHOTO_COUNT_FILE_PATH, self.photo_count_entry.text(), "Ошибка сохранения кол-ва фото")

    def load_tokens_ui_update(self):
        if os.path.exists(TOKENS_FILE_PATH):
            try:
                with open(TOKENS_FILE_PATH, 'r', encoding='utf-8') as file: self.tokens = [token.strip() for token in file.readlines() if token.strip()]
                self.tokens_label.setText(f"Токенов загружено: {len(self.tokens)}")
                if self.tokens: self.log_message_slot(f"Загружено {len(self.tokens)} токенов.\n")
                else: self.log_message_slot("Файл токенов пуст.\n")
            except Exception as e:
                self.log_message_slot(f"Ошибка чтения файла токенов: {e}\n")
                QMessageBox.critical(self, "Ошибка токенов", f"Не удалось прочитать файл токенов: {e}")
                self.tokens = []; self.tokens_label.setText("Токенов: 0 (ошибка)")
        else:
            self.log_message_slot(f"Файл с токенами не найден: {TOKENS_FILE_PATH}\n")
            self.tokens_label.setText("Токенов: 0 (файл не найден)")
        self.update_buttons_on_token_load()

    def update_buttons_on_token_load(self):
        has_tokens = bool(self.tokens)
        self.post_button.setEnabled(has_tokens)
        self.remove_schedule_button.setEnabled(has_tokens and bool(self.posting_schedule))
        self.autopost_check.setEnabled(has_tokens)
        if not has_tokens: self.autopost_check.setChecked(False)
        self.update_add_schedule_button_state()
        self.update_retry_button_state()

    def update_add_schedule_button_state(self):
        is_autopost = self.autopost_check.isChecked()
        self.add_schedule_button.setEnabled(not is_autopost and bool(self.tokens))
        self.post_limit_entry.setEnabled(is_autopost and bool(self.tokens))
        self.photo_count_entry.setEnabled(is_autopost and bool(self.tokens))
        if is_autopost and self.posting_schedule:
            self.log_message_slot("Режим автопубликации включен. Ручной план публикаций очищен.\n")
            self.posting_schedule.clear()
            self.update_posting_schedule_table_display()
        self.update_retry_button_state() # Also update retry button as it depends on posting state

    def update_retry_button_state(self):
        can_retry = bool(self.other_error_tokens) and not (self.posting_thread and self.posting_thread.isRunning())
        self.retry_other_errors_button.setEnabled(can_retry)

    def open_add_posting_set_dialog(self):
        dialog = AddPostingSetDialog(self); dialog.exec_()

    def add_posting_set_data(self, data):
        self.posting_schedule.append(data)
        self.update_posting_schedule_table_display()
        self.update_buttons_on_token_load()

    def remove_selected_posting_sets(self):
        selected_rows = sorted(list(set(item.row() for item in self.posting_schedule_table.selectedItems())), reverse=True)
        if not selected_rows:
            QMessageBox.warning(self, "Предупреждение", "Выберите запись для удаления.")
            return
        removed_count = 0
        for row_index in selected_rows:
            if 0 <= row_index < len(self.posting_schedule):
                del self.posting_schedule[row_index]
                self.log_message_slot(f"Удалена публикация (бывший №{row_index + 1} в списке до удаления).\n")
                removed_count +=1
        if removed_count > 0: self.update_posting_schedule_table_display()
        self.update_buttons_on_token_load()

    def update_posting_schedule_table_display(self):
        self.posting_schedule_table.setRowCount(0)
        for idx, entry in enumerate(self.posting_schedule):
            self.posting_schedule_table.insertRow(idx)
            photos_count = len(entry.get('photos', [])); videos_count = len(entry.get('videos', []))
            text_preview = (entry['text'][:30].replace(os.linesep, ' ') + '...') if len(entry['text']) > 30 else entry['text'].replace(os.linesep, ' ')
            self.posting_schedule_table.setItem(idx, 0, QTableWidgetItem(str(idx + 1)))
            self.posting_schedule_table.setItem(idx, 1, QTableWidgetItem(f"{photos_count} фото"))
            self.posting_schedule_table.setItem(idx, 2, QTableWidgetItem(f"{videos_count} видео"))
            self.posting_schedule_table.setItem(idx, 3, QTableWidgetItem(text_preview))
        self.posting_schedule_table.resizeColumnsToContents()
        self.posting_schedule_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)

    def submit_post_handler(self):
        if self.posting_thread and self.posting_thread.isRunning():
            self.log_message_slot("Процесс публикации уже запущен.\n")
            return
        self.clear_previous_results() # Clears specific token sets too
        self.final_failed_tokens_for_ui_copy.clear(); self.final_successful_tokens_for_ui_copy.clear()
        
        self.posting_start_time = time.monotonic() 
        self.elapsed_seconds = 0
        self.elapsed_time_label.setText("Время: 00:00:00")
        self.runtime_qtimer.start(1000) 

        post_limit = 0; photo_count = 0; is_autopost = self.autopost_check.isChecked()
        if is_autopost:
            try:
                post_limit = int(self.post_limit_entry.text()); photo_count = int(self.photo_count_entry.text())
                if post_limit <= 0 or photo_count <= 0:
                    QMessageBox.critical(self, "Ошибка", "Лимит постов и количество фотографий должны быть больше 0.")
                    self.runtime_qtimer.stop() 
                    return
            except ValueError:
                QMessageBox.critical(self, "Ошибка", "Введите корректные числовые значения для лимита постов и количества фотографий.")
                self.runtime_qtimer.stop() 
                return
        self.posting_thread = PostingWorker(
            list(self.tokens), # Use all tokens for a normal run
            list(self.posting_schedule), 
            is_autopost, 
            post_limit, 
            photo_count, 
            self.worker_signals
            )
        self.posting_thread.start()
        self.progress_bar.setValue(0)

    def retry_other_errors_handler(self):
        if self.posting_thread and self.posting_thread.isRunning():
            self.log_message_slot("Процесс публикации уже запущен.\n")
            return

        if not self.other_error_tokens:
            self.log_message_slot("Нет токенов в категории 'Прочие ошибки' для повтора.\n")
            return

        tokens_for_retry = list(self.other_error_tokens) 

        self.log_message_slot(f"Запуск повторной публикации для {len(tokens_for_retry)} токенов из 'Прочих ошибок'...\n")
        
        # Store current other_error_tokens before clearing, then restore if needed or adjust logic
        # For now, clear_previous_results will wipe them for the new run.
        self.clear_previous_results() 
        self.final_failed_tokens_for_ui_copy.clear() # Also clear these global sets for a new run
        self.final_successful_tokens_for_ui_copy.clear()
            
        is_autopost = self.autopost_check.isChecked()
        posting_schedule_for_retry = []
        post_limit = 0
        photo_count = 0

        if is_autopost:
            try:
                post_limit = int(self.post_limit_entry.text())
                photo_count = int(self.photo_count_entry.text())
                if post_limit <= 0 or photo_count <= 0:
                    QMessageBox.critical(self, "Ошибка", "Лимит постов и количество фотографий для автопостинга должны быть больше 0.")
                    self.update_retry_button_state() # Re-enable if validation fails
                    return
            except ValueError:
                QMessageBox.critical(self, "Ошибка", "Введите корректные числовые значения для лимита постов и количества фотографий для автопостинга.")
                self.update_retry_button_state() # Re-enable if validation fails
                return
            # posting_schedule_for_retry remains empty; worker generates it for autopost
        else: 
            if not self.posting_schedule:
                self.log_message_slot("Нет наборов публикаций в ручном режиме для повтора.\n")
                QMessageBox.information(self, "Повтор", "План публикаций пуст. Добавьте наборы для публикации.")
                self.update_retry_button_state() # Re-enable
                return
            posting_schedule_for_retry = list(self.posting_schedule)

        self.posting_start_time = time.monotonic()
        self.elapsed_seconds = 0
        self.elapsed_time_label.setText("Время: 00:00:00")
        self.runtime_qtimer.start(1000)

        self.posting_thread = PostingWorker(
            tokens_for_retry,
            posting_schedule_for_retry, 
            is_autopost,
            post_limit, 
            photo_count, 
            self.worker_signals
        )
        self.posting_thread.start()
        self.progress_bar.setValue(0)
        # toggle_posting_buttons_slot(False) will be called by the worker's enable_controls signal

    def stop_posting_handler(self):
        if self.posting_thread and self.posting_thread.isRunning():
            stop_flag.set()
            self.log_message_slot("Остановка публикации...\n")
            self.stop_button.setEnabled(False)

    def clear_previous_results(self):
        self.output_text.clear(); self.successful_text.clear(); self.failed_text.clear()
        self.successful_posts_label.setText("Успешные публикации: 0")
        self.failed_posts_label.setText("Неуспешные публикации: 0")
        
        self.token_error_specific_count = 0
        self.banned_token_specific_count = 0
        self.other_error_specific_count = 0 
        self.token_errors_specific_label.setText("Ошибка авторизации (токен): 0")
        self.banned_tokens_specific_label.setText("Аккаунт заблокирован: 0")
        self.other_errors_specific_label.setText("Прочие ошибки: 0") 

        self.auth_error_tokens.clear()
        self.banned_error_tokens.clear()
        self.other_error_tokens.clear()
        
        self.elapsed_time_label.setText("Время: 00:00:00") 
        self.elapsed_seconds = 0
        self.update_retry_button_state()

    def log_message_slot(self, message):
        self.output_text.append(message.strip())
        if self.auto_scroll_log: self.output_text.verticalScrollBar().setValue(self.output_text.verticalScrollBar().maximum())

    def update_progress_labels_slot(self, success_count, failed_count, total_expected_posts):
        self.successful_posts_label.setText(f"Успешные публикации: {success_count}")
        self.failed_posts_label.setText(f"Неуспешные публикации: {failed_count}") 
        remaining = max(0, total_expected_posts - (success_count + failed_count))
        self.progress_label.setText(f"Осталось попыток постов (из общего плана): {remaining}")

    def successful_post_slot(self, message, link):
        self.successful_text.append(f"{message} <a href='{link}' style='color: #6897bb;'>{link}</a><br>")
        # Обновляем дату последней публикации при каждой успешной публикации
        self.save_last_post_date()

    def failed_post_slot(self, message, token): 
        self.failed_text.append(message.strip())

        is_token_error = "User authorization failed: invalid access_token (4)" in message
        is_banned_error = ("account has been blocked" in message.lower() or
                           "user is blocked" in message.lower() or
                           "user was banned" in message.lower())

        if token: # Ensure token is not None before adding
            if is_token_error:
                self.token_error_specific_count += 1
                self.token_errors_specific_label.setText(f"Ошибка авторизации (токен): {self.token_error_specific_count}")
                self.auth_error_tokens.add(token)
            elif is_banned_error: 
                self.banned_token_specific_count += 1
                self.banned_tokens_specific_label.setText(f"Аккаунт заблокирован: {self.banned_token_specific_count}")
                self.banned_error_tokens.add(token)
            else: 
                self.other_error_specific_count += 1
                self.other_errors_specific_label.setText(f"Прочие ошибки: {self.other_error_specific_count}")
                self.other_error_tokens.add(token)
        
        self.update_retry_button_state()

    def toggle_posting_buttons_slot(self, enable_main_actions):
        self.post_button.setEnabled(enable_main_actions)
        self.stop_button.setEnabled(not enable_main_actions)
        
        self.add_schedule_button.setEnabled(enable_main_actions and not self.autopost_check.isChecked())
        self.remove_schedule_button.setEnabled(enable_main_actions and bool(self.posting_schedule))
        self.account_button.setEnabled(enable_main_actions)
        self.autopost_check.setEnabled(enable_main_actions)
        
        if enable_main_actions and self.autopost_check.isChecked():
             self.post_limit_entry.setEnabled(True)
             self.photo_count_entry.setEnabled(True)
        elif not enable_main_actions and self.autopost_check.isChecked(): # Posting in progress, autopost on
            self.post_limit_entry.setEnabled(False)
            self.photo_count_entry.setEnabled(False)

        self.update_retry_button_state()

    def on_posting_finished(self):
        self.runtime_qtimer.stop() 
        if self.posting_thread:
            self.final_failed_tokens_for_ui_copy = self.posting_thread.get_final_failed_tokens()
            self.final_successful_tokens_for_ui_copy = self.posting_thread.get_final_successful_tokens()
        
        # Call toggle_posting_buttons_slot which will in turn call update_retry_button_state
        self.toggle_posting_buttons_slot(True) 
        
        self.posting_thread = None
        stop_flag.clear()
        if self.progress_bar.value() < self.progress_bar.maximum():
             self.progress_bar.setValue(self.progress_bar.maximum())

    def closeEvent(self, event):
        if self.posting_thread and self.posting_thread.isRunning():
            reply = QMessageBox.question(self, "Подтверждение",
                                         "Процесс публикации еще активен. Вы уверены, что хотите выйти?",
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                stop_flag.set(); 
                if self.posting_thread: self.posting_thread.wait(5000)
                self.runtime_qtimer.stop() 
                event.accept()
            else: event.ignore()
        else: 
            self.runtime_qtimer.stop() 
            event.accept()

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    QStyleFactory.create('Fusion')
    app.setStyleSheet(DARK_STYLESHEET)
    mainWin = VKAutoPosterApp()
    mainWin.show()
    sys.exit(app.exec_())
